Skip to content

Support streaming tool output and deduplication#7

Draft
timvisher-dd wants to merge 8 commits intomainfrom
streaming-dedup
Draft

Support streaming tool output and deduplication#7
timvisher-dd wants to merge 8 commits intomainfrom
streaming-dedup

Conversation

@timvisher-dd
Copy link
Owner

Closes xenodium#342
Closes xenodium#343

Checklist

  • I agree to communicate (PR description and comments) with the author myself (not AI-generated).
  • I've reviewed all code in PR myself and will vouch for its quality.
  • I've read and followed the Contributing guidelines.
  • I've filed a feature request/discussion for a new feature.
  • I've added tests where applicable.
  • I've run M-x checkdoc and M-x byte-compile-file.

Problem

The two most popular agent ACPs, codex-acp and claude-agent-acp, perform very poorly in agent-shell when tool executions emit a lot of text:

  • codex-acp: O(n²) rendering and massive data transfer. A 35k-line bash command takes ~60s and transfers ~890 MB of JSON — a 3,000× amplification of ~280 KB actual output. Each tool_call_update carries the full accumulated output; agent-shell replaces the entire fragment body and reruns markdown-overlays-put on every update.

  • claude-agent-acp: output is silently lost. The same command truncates to 241 of 35,001 lines. The user sees raw <persisted-output> XML tags rendered verbatim in the shell buffer.

Cause

agent-shell does not advertise _meta.terminal_output in clientCapabilities during the ACP initialize handshake. Without this capability:

  • codex-acp falls back to sending the full accumulated output in every tool_call_update (O(n²) content growth).
  • claude-agent-acp sends a single truncated result at completion instead of streaming the full output.

Fix

Advertise _meta.terminal_output during initialize and handle the resulting streaming behavior:

  1. Extend acp.el to accept :terminal-capability and :meta-capabilities on acp-make-initialize-request.
  2. Pass those capabilities from agent-shell.el during initialize.
  3. Handle incremental _meta.terminal_output.data chunks (codex-acp) and batch _meta.terminal_output results (claude-agent-acp) in a new streaming handler with deduplication.
  4. Strip <persisted-output> tags and render previews cleanly.

Implementation

New files

agent-shell-meta.el — extractors for ACP _meta payloads:

  • agent-shell--meta-lookup — key lookup handling both symbol and string keys in alists.
  • agent-shell--meta-find-tool-response — walks any _meta namespace to find a toolResponse value.
  • agent-shell--tool-call-meta-response-text — extracts stdout text from _meta.*.toolResponse in its various shapes (string, alist with stdout key, vector of content blocks).
  • agent-shell--tool-call-terminal-output-data — extracts _meta.terminal_output.data.

agent-shell-streaming.el — streaming tool call update handler:

  • agent-shell--tool-call-normalize-output — strips markdown fences, strips <persisted-output> XML tags (rendering the preview with font-lock-comment-face), and ensures trailing newlines.
  • agent-shell--append-tool-call-output — accumulates streamed output in the state's :tool-calls hash under an :accumulated key per tool call ID.
  • agent-shell--handle-tool-call-update-streaming — the main handler, replacing the inline tool_call_update block in agent-shell.el. Three branches:
    1. Terminal data (_meta.terminal_output.data): normalize the chunk, accumulate it, and immediately append it to the fragment body for live streaming.
    2. Meta response (_meta.*.toolResponse): normalize and accumulate silently (rendered only on final update to avoid duplication).
    3. Final update (status is "completed" or "failed"): render accumulated output (or fall back to content text), log to transcript, clean up permission dialogs, and apply title/label updates.
  • agent-shell--mark-tool-calls-cancelled — marks all in-progress tool calls as cancelled (called from agent-shell-interrupt).

Changes to agent-shell.el

  • (require 'agent-shell-streaming) added.
  • The ~50-line inline tool_call_update rendering block is replaced by a single call to agent-shell--handle-tool-call-update-streaming. The metadata save (title/description/command/raw-input/diff) remains inline before the handler call.
  • The initialize request now passes :terminal-capability t and :meta-capabilities '((terminal_output . t)) to acp-make-initialize-request.
  • agent-shell-interrupt calls agent-shell--mark-tool-calls-cancelled after sending the cancel notification.
  • shell-maker-define-major-mode call passes 'agent-shell-mode-map (quoted symbol) instead of the bare variable.

Tests

7 new tests in tests/agent-shell-streaming-tests.el:

  • agent-shell--tool-call-meta-response-text-test — extracts text from _meta.claudeCode.toolResponse.stdout.
  • agent-shell--tool-call-normalize-output-test — strips fences and ensures trailing newline.
  • agent-shell--tool-call-normalize-output-persisted-output-test — strips <persisted-output> tags.
  • agent-shell--tool-call-update-writes-output-test — verifies accumulated output is written to the fragment body.
  • agent-shell--tool-call-meta-response-no-duplication-test — meta response text is rendered once, not duplicated with content.
  • agent-shell-initialize-request-meta-capabilities-test — the initialize request includes _meta.terminal_output.
  • agent-shell--tool-call-terminal-output-data-streaming-test — codex-style _meta.terminal_output.data chunks are accumulated and rendered incrementally.

Perf measurements

Test: for x in {0..35000}; do printf 'line %d\n' "$x"; done (35,001 lines)

codex-acp

measure_ms (avg) content_bytes (avg) terminal_bytes (avg)
Without terminal caps ~60,000 ~900,000,000 0
With terminal caps ~7,500 ~3,000 ~240,000

~8× faster. Content drops from ~900 MB to ~3 KB.

claude-agent-acp

measure_ms (avg) content_bytes terminal_bytes
Without terminal caps ~22,000 2,321 (truncated to 241 lines) 0
With terminal caps ~23,000 0 2,270

No timing improvement (execution is server-side), but <persisted-output> tags are handled cleanly.

Prerequisite: acp.el changes

acp.el needs to accept :terminal-capability and :meta-capabilities keyword arguments on acp-make-initialize-request. See xenodium/acp.el#15.

@timvisher-dd timvisher-dd force-pushed the streaming-dedup branch 2 times, most recently from 3415d07 to 9101647 Compare March 16, 2026 14:26
@timvisher-dd timvisher-dd changed the title # Support streaming tool output and deduplication Support streaming tool output and deduplication Mar 16, 2026
@timvisher-dd timvisher-dd force-pushed the streaming-dedup branch 6 times, most recently from df9a77f to 68b7774 Compare March 19, 2026 15:07
timvisher-dd and others added 8 commits March 20, 2026 10:08
Point .claude, .codex, .gemini directories to .agents and their
respective markdown files to AGENTS.md so all IDE agents share a
single source of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI runs four jobs: byte-compilation + ERT tests, agent config symlink
verification, dependency DAG cycle detection, and README update check.
bin/test drives all checks locally by parsing ci.yml with yq so the
two stay in sync automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ning

- Quote agent-shell-mode-map symbol in shell-maker-define-major-mode
  (macros expect the symbol name, not the variable value)
- Fix decorator tests: use setq-local inside let instead of let*
  shadowing the buffer-local, so buffer-local-value finds the binding
- Suppress message output in copy-session-id test
- Add forward declaration for agent-shell-text-file-capabilities in
  devcontainer module to silence byte-compiler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New module for checking buffer-level invariants during agent-shell
operation: process-mark ordering, fragment ordering per namespace,
ui-state property contiguity, and content-store consistency.

Includes a per-buffer event ring for tracing, rate-limited violation
reporting with debug bundle capture, and comprehensive ERT tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New utility module for walking nested _meta namespaces in ACP tool
call updates.  Handles multiple agent response shapes (stdout string,
content string, vector of text items) and provides clean accessors
for toolResponse text and streaming terminal output data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New module for incremental tool call output rendering.  Accumulates
chunks from _meta.*.toolResponse and _meta.terminal_output, strips
backtick fences and <persisted-output> tags, and appends deltas
in-place to avoid O(n²) full-block rebuilds during streaming.

Includes per-tool-call output markers, UI state caching, and
comprehensive tests covering codex-acp terminal output, claude-agent
batch results, and error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire the new streaming, meta, and invariants libraries into the core
agent-shell and UI layers:

- Advertise _meta.terminal_output capability in session/new
- Extract tool-call update handler into agent-shell-streaming
- Deduplicate thought chunks via accumulated-delta tracking
- Add insert-cursor (marker with insertion-type t) so fragments appear
  in creation order above the prompt
- Preserve process-mark across fragment updates so context insertion
  and prompt position remain stable
- Debounce markdown overlay application during streaming appends to
  avoid O(n²) re-parsing
- In-place body append in agent-shell-ui to avoid delete-and-reinsert
  that displaces point
- Fix context insertion: goto-char insert-start so point lands at the
  prompt after inserting context
- Add context insertion regression tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add live-validate command documentation for verifying rendering
  pipeline changes with a live batch session
- Update AGENTS.md development workflow with live-validate step
- Update README.org features list: expand CI sub-features, add
  streaming dedup, DWIM context insertion, and runtime invariants

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support streaming tool output and deduplication

1 participant